﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using CIP4.Common;

namespace CIP4.Item
{
    public class CIP4ItemVersionsType
    {
        private readonly long _version;

        private readonly object _currentState;

        private readonly SortedDictionary<long, CIP4ItemVersionType> _versions;

        public SortedDictionary<long, CIP4ItemVersionType> Versions
        {
            get { return _versions; }
        }

        public CIP4ItemVersionsType()
        {
            _versions = new SortedDictionary<long, CIP4ItemVersionType>();
            _version = DateTime.Now.Ticks;
        }

        public CIP4ItemVersionsType(object current)
        {
            _versions = new SortedDictionary<long, CIP4ItemVersionType>();
            _version = DateTime.Now.Ticks;
            _currentState = current;
        }

        public CIP4ItemVersionsType(DateTime versionTimestamp, object current)
        {
            _versions = new SortedDictionary<long, CIP4ItemVersionType>();
            _version = versionTimestamp.Ticks;
            _currentState = current;
        }

        public void AddValue(string propertyName, object propertyValue)
        {
            if (!Versions.ContainsKey(_version))
                Versions.Add(_version, new CIP4ItemVersionType());
            if (!Versions[_version].Values.ContainsKey(propertyName))
                Versions[_version].Values.Add(propertyName, propertyValue);
            else
                Versions[_version].Values[propertyName] = propertyValue;
        }

        public void AddVersion(string versionId, Dictionary<string, object> values)
        {
            Versions.Add(long.Parse(versionId), new CIP4ItemVersionType(values));
        }

        public delegate bool IsEqualDelegate(object one, object two);

        /// <summary>
        /// Idea is to build stgructures that can be used to make the oldValues list given the newValues list
        /// There are three possibilities:
        /// 1)  The newValues list contains something not in the old values list (i.e. a value has been added)
        ///     the value must be deleted to reconstitute the oldValues
        /// 2)  The oldValues list contains something that is not in the newValues list (i.e. a value has been deleted)
        ///     the value must be saved so it can be added in order to reconstitute the oldValue
        /// 3)  A value has been changed - This is handled as a 1 followed by a 2
        /// 
        /// There are three lists maintained in the version record
        /// 1) a list called -propertyName- this has the saved values from #2
        /// 2) a list caled -propertyName_added - this has the index where the -propertyName- items are to be added
        /// 3) a list caled -propertyName_deleteThese - this has the index of items from #1 that have to be deleted
        /// </summary>
        /// <typeparam name="TT">Type the element in the MV things</typeparam>
        /// <param name="propertyName"></param>
        /// <param name="newValues">New state of the property</param>
        /// <param name="oldValues">Old state of the property</param>
        /// <param name="isEqual">Delegate that returns true if the two value are equal</param>
        /// <param name="serializeToXElement"></param>
        public void MakeMvDelta<TT>(string propertyName, List<TT> newValues, List<TT> oldValues, IEqualityComparer<TT> isEqual, SerializeToDataDelegate serializeToXElement)
        {
            if (oldValues == null && newValues == null)
                return;


            if (!(oldValues == null || newValues == null) && oldValues.Count == newValues.Count)
            {
                bool allTheSame = true;
                for(int index = 0; index < oldValues.Count; index++)
                    if (!isEqual.Equals(oldValues[index], newValues[index]))
                    {
                        allTheSame = false;
                        break;
                    }
                if (allTheSame)
                    return;
            }

            int largestCount =
                newValues != null && oldValues != null
                    ? Math.Min(oldValues.Count, newValues.Count)
                    : 0;

            // Do deleteThese

            List<int> deleteThese = new List<int>();

            for (int index = 0; index < (newValues == null? 0 : newValues.Count); index++)
            {
                if (index < (oldValues == null? 0 : oldValues.Count))
                {
                    if (!isEqual.Equals(newValues[index], oldValues[index]))
                        deleteThese.Add(index);
                }
                else
                {
                    deleteThese.Add(index);
                }
            }


            // Do values list and added

            List<int> added = new List<int>();
            List<TT> values = new List<TT>();

            for(int index = 0; index < (oldValues == null? 0 : oldValues.Count); index++)
            {
                if (index < (newValues == null? 0 : newValues.Count))
                {
                    if (!isEqual.Equals(newValues[index], oldValues[index]))
                    {
                        added.Add(index);
                        values.Add(oldValues[index]);
                    }
                }
                else
                {
                    added.Add(index);
                    values.Add(oldValues[index]);
                }
            }

            if (!Versions.ContainsKey(_version))
                Versions.Add(_version, new CIP4ItemVersionType());

            if (added.Count > 0)
            {
                if (!Versions[_version].Values.ContainsKey(propertyName))
                    Versions[_version].Values.Add(propertyName, values);
                else
                    Versions[_version].Values[propertyName] = values;

                if (!Versions[_version].Values.ContainsKey(propertyName + "_added"))
                    Versions[_version].Values.Add(propertyName + "_added", added);
                else
                    Versions[_version].Values[propertyName + "_added"] = added;
            }

            if (deleteThese.Count > 0)
            {
                if (!Versions[_version].Values.ContainsKey(propertyName + "_deleteThese"))
                    Versions[_version].Values.Add(propertyName + "_deleteThese", deleteThese);
                else
                    Versions[_version].Values[propertyName + "_deleteThese"] = deleteThese;
            }
        }

        public void MakeDictionaryDelta<TK, TT>(string propertyName, Dictionary<TK, TT> newValues, Dictionary<TK, TT> oldValues, IEqualityComparer<TT> isEqual, SerializeToDataDelegate serializeToXElement)
        {
            if (newValues == null && oldValues == null)
                return;

            // entries that have to be added back in to reconstitute this version
            Dictionary<TK, object> deletedEntries =
                newValues == null ?
                oldValues.ToDictionary<KeyValuePair<TK, TT>, TK, object>(kvp => kvp.Key, kvp => kvp.Value) :
                oldValues != null ?
                oldValues.Where(kvp => !newValues.ContainsKey(kvp.Key)).ToDictionary(
                    kvp => kvp.Key,
                    kvp => serializeToXElement == null ? kvp.Value : serializeToXElement(kvp.Value, null)) :
                new Dictionary<TK, object>();

            // entries that have been added and should be deleted to reconstitute this version
            List<TK> addedKeys =
                newValues == null ? new List<TK>() :
                oldValues != null ?
                newValues.Keys.Where(key => !oldValues.ContainsKey(key)).ToList() :
                newValues.Keys.ToList();

            if (addedKeys.Count == 0 && deletedEntries.Count == 0)
                return;

            if (!Versions.ContainsKey(_version))
                Versions.Add(_version, new CIP4ItemVersionType());

            if (deletedEntries.Count > 0)
            {
                if (!Versions[_version].Values.ContainsKey(propertyName))
                    Versions[_version].Values.Add(propertyName, deletedEntries);
                else
                    Versions[_version].Values[propertyName] = deletedEntries;
            }

            if (addedKeys.Count > 0)
            {
                if (!Versions[_version].Values.ContainsKey(propertyName + "_deleteThese"))
                    Versions[_version].Values.Add(propertyName + "_deleteThese", addedKeys);
                else
                    Versions[_version].Values[propertyName + "_deleteThese"] = addedKeys;
            }
        }

        public T GetCurrent<T>()
        {
            return (T)_currentState;
        }

        public SortedDictionary<DateTime, T> GetItemVersions<T>(ISerializerType serializer)
        {

            SortedDictionary<DateTime, T> itemVersions = 
                new SortedDictionary<DateTime, T> {{DateTime.Now, GetCurrent<T>()}};
            T last = GetCurrent<T>();
            foreach (KeyValuePair<long,  CIP4ItemVersionType> kvp in Versions.Reverse())
            {
                last = kvp.Value.GetItem(last, serializer);
                itemVersions.Add(new DateTime(kvp.Key), last );
            }
            return itemVersions;
        }

    }

    public class CIP4ItemVersionType
    {
        public Dictionary<string, object> Values { get; private set; }

        public CIP4ItemVersionType()
        {
            Values = new Dictionary<string, object>();
        }

        public CIP4ItemVersionType(Dictionary<string, object> values)
        {
            Values = values;
        }

        public T GetItem<T>(T previousVersion, ISerializerType serializer)
        {
            object objectData = serializer.GetDataForObject(previousVersion);
            T nextVersion = (T)serializer.GetObjectForData(objectData);
            foreach (KeyValuePair<string, object> kvp in Values)
            {
                if (kvp.Key.EndsWith("_deleteThese"))
                {
                    PropertyInfo prop = typeof(T).GetProperty(kvp.Key.Substring(0, kvp.Key.Length - "_deleteThese".Length));
                    MvPropertyDelete(prop, nextVersion, kvp.Value);
                }
            }
            foreach (KeyValuePair<string, object> kvp in Values)
            {
                PropertyInfo prop = typeof(T).GetProperty(kvp.Key);
                if (prop != null)
                {
                    if (kvp.Value is IList)
                        MvPropertyAdd(prop, previousVersion, nextVersion, kvp.Value, Values[kvp.Key + "_added"]);
                    else
                        prop.SetValue(nextVersion, kvp.Value, null);
                }
            }
            return nextVersion;
        }

        public void MvPropertyAdd(PropertyInfo prop, object previousVersion, object nextVersion, object values, object indexesObject)
        {
            IList indexes = (IList) indexesObject;
            IList previousValues = (IList)prop.GetValue(previousVersion, new object[] { });
            IList nextValues = (IList)prop.GetValue(nextVersion, new object[] { });
            if (previousValues == null || previousValues.Count == 0)
            {
                prop.SetValue(nextVersion, values, null);
                return;
            }
            IList valueList = (IList) values;
            for (int index = 0; index < valueList.Count; index++)
                nextValues.Insert((int)indexes[index], valueList[index]);
        }

        public void MvPropertyDelete(PropertyInfo prop, object nextVersion, object indexes)
        {
            IList existingValues = (IList)prop.GetValue(nextVersion, new object[] { });
            if (existingValues == null || existingValues.Count == 0)
            {
                throw new ArgumentException(string.Format("Version data is corrupt in {0}", nextVersion));
            }

            IList indexesList = (IList)indexes;

            for(int index = indexesList.Count - 1; index >= 0; index--)
            {
                existingValues.RemoveAt((int) indexesList[index]);
            }
        }
    }
}
